﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using ROrganizer.Client.Files;

namespace ROrganizer.Client
{
    public class MetaFacility
    {
        public MetaEntry AnalyzeFile(string file)
        {
            using (var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                var info = this.AnalyzeFile(stream);

                if (info == null)
                    return null;

                return new MetaEntry
                {
                    File = new FileEntry
                    {
                        Path = file.ToLower(),
                        Timestamp = FileEnumerator.GetFileTimestmap(file)
                    },
                    Info = new MetaInfo
                    {
                        Size = FileEnumerator.GetFileSize(file),
                        Runtime = info.Runtime,
                        Width = info.Width,
                        Height = info.Height,
                    }
                };
            }
        }

        public MetaInfo AnalyzeFile(Stream stream)
        {
            var avi = Encoding.ASCII.GetBytes("RIFF");
            var mkv = new byte[] { 0x1a, 0x45, 0xdf, 0xa3 };
            var buffer = new byte[4];

            stream.Read(buffer, 0, 4);
            stream.Seek(0, SeekOrigin.Begin);

            if (buffer.SequenceEqual(avi))
                return this.AnalyzeAviFile(stream);

            if (buffer.SequenceEqual(mkv))
                return this.AnalyzeMatroskaFile(stream);

            return null;
        }

        public string GetExtension(Stream stream)
        {
            var avi = Encoding.ASCII.GetBytes("RIFF");
            var mkv = new byte[] { 0x1a, 0x45, 0xdf, 0xa3 };
            var buffer = new byte[4];

            stream.Read(buffer, 0, 4);

            if (buffer.SequenceEqual(avi))
                return ".avi";

            if (buffer.SequenceEqual(mkv))
                return ".mkv";

            return null;
        }

        private MetaInfo AnalyzeAviFile(Stream stream)
        {
            using (var parser = new RiffParser(stream))
            {
                var block = parser.GetBlock("AVI ", "hdrl", "avih");
                var extra = parser.GetBlock("AVI ", "hdrl", "strl", "strh");

                if (block == null || extra == null)
                    return null;

                var rate = parser.ReadLong(block, 0);
                var length = parser.ReadLong(extra, 32);

                return new MetaInfo
                {
                    Width = (int)parser.ReadLong(block, 32),
                    Height = (int)parser.ReadLong(block, 36),
                    Runtime = (int)Math.Round(length * (rate / 1000000M))
                };
            }
        }

        private MetaInfo AnalyzeMatroskaFile(Stream stream)
        {
            using (var parser = new MatroskaParser(stream))
            {
                var wblock = parser.GetBlock(0x8538067, 0x654ae6b, 0x2e, 0x60, 0x30);
                var hblock = parser.GetBlock(0x8538067, 0x654ae6b, 0x2e, 0x60, 0x3a);
                var tblock = parser.GetBlock(0x8538067, 0x549a966, 0xad7b1);
                var dblock = parser.GetBlock(0x8538067, 0x549a966, 0x489);

                if (wblock == null || hblock == null || tblock == null || dblock == null)
                    return null;

                return new MetaInfo
                {
                    Width = (int)parser.ReadLong(wblock),
                    Height = (int)parser.ReadLong(hblock),
                    Runtime = (int)Math.Round(parser.ReadLong(tblock) * parser.ReadFloat(dblock) / 1000000000M)
                };
            }
        }

        private class RiffParser : IDisposable
        {
            private readonly Stream inner;
            private readonly BufferedStream buffered;

            public RiffParser(Stream file, int? buffsize = null)
            {
                this.inner = file;
                this.buffered = new BufferedStream(file, buffsize ?? 8192);
            }

            public byte[] GetBlock(params string[] path)
            {
                var position = 0;
                buffered.Seek(0, SeekOrigin.Begin);

                while (buffered.Position < buffered.Length)
                {
                    var header = this.ReadString(4);
                    var size = this.ReadLong();

                    if (header == "RIFF" || header == "LIST")
                        header = this.ReadString(4);

                    if (header == path[position] && path.Length == position + 1)
                        return this.ReadBytes((int)size);

                    if (header == path[position] && path.Length > position + 1)
                    {
                        position++;
                        continue;
                    }

                    buffered.Seek(size + size % 2, SeekOrigin.Current);
                }

                return null;
            }

            private string ReadString(int bytes, bool rewind = false)
            {
                var buffer = this.ReadBytes(bytes, rewind);
                return Encoding.UTF8.GetString(buffer);
            }

            private long ReadLong(bool rewind = false)
            {
                return this.ReadLong(this.ReadBytes(4, rewind));
            }

            public long ReadLong(byte[] array, int index = 0)
            {
                var result = 0L;

                for (int i = 3 + index; i >= index; i--)
                    result = result * 256 + array[i];

                return result;
            }

            private byte[] ReadBytes(int count, bool rewind = false)
            {
                var buffer = new byte[count];
                
                while (count > 0)
                    count -= this.buffered.Read(buffer, buffer.Length - count, count);

                if (rewind)
                    this.buffered.Seek(-buffer.Length, SeekOrigin.Current);

                return buffer;
            }

            void IDisposable.Dispose()
            {
            }
        }

        private class MatroskaParser : IDisposable
        {
            private readonly Stream inner;
            private readonly BufferedStream buffered;

            public MatroskaParser(Stream file, int? buffsize = null)
            {
                this.inner = file;
                this.buffered = new BufferedStream(file, buffsize ?? 8192);
            }

            public byte[] GetBlock(params long[] path)
            {
                var position = 0;
                buffered.Seek(0, SeekOrigin.Begin);

                while (buffered.Position < buffered.Length)
                {
                    var id = this.ReadLong();
                    var size = this.ReadLong();

                    if (id == path[position] && path.Length == position + 1)
                        return this.ReadBytes((int)size);

                    if (id == path[position] && path.Length > position + 1)
                    {
                        position++;
                        continue;
                    }

                    buffered.Seek(size, SeekOrigin.Current);
                }

                return null;
            }

            private long ReadLong()
            {
                var first = (byte)this.buffered.ReadByte();
                var leading = this.GetNumberOfLeedingZeros(first);
                var stripped = this.StripLeadingBits(first, leading + 1);
                var rest = this.ReadBytes(leading);
                var array = new[] { stripped }.Concat(rest).ToArray();
                var result = 0L;

                for (int i = 0; i < array.Length; i++)
                    result = result * 256 + array[i];

                return result;
            }

            public long ReadLong(byte[] array, bool variable = false)
            {
                if (variable)
                {
                    var leading = this.GetNumberOfLeedingZeros(array[0]);
                    var stripped = this.StripLeadingBits(array[0], leading + 1);
                    array[0] = stripped;
                }

                var result = 0L;
                for (int i = 0; i < array.Length; i++)
                    result = result * 256 + array[i];

                return result;
            }

            public decimal ReadFloat(byte[] array)
            {
                if (BitConverter.IsLittleEndian)
                    Array.Reverse(array);

                if (array.Length == 4)
                    return (decimal)BitConverter.ToSingle(array, 0);

                if (array.Length == 8)
                    return (decimal)BitConverter.ToDouble(array, 0);

                return 0;
            }

            private int GetNumberOfLeedingZeros(byte value)
            {
                var counter = 0;
                var mask = 128;

                while (mask > 0 && (value & mask) == 0)
                {
                    counter++;
                    mask /= 2;
                }

                return counter;
            }

            private byte StripLeadingBits(byte value, int count)
            {
                return (byte)(value & ((1 << (8 - count)) - 1));
            }

            private byte[] ReadBytes(int count, bool rewind = false)
            {
                var buffer = new byte[count];

                while (count > 0)
                    count -= this.buffered.Read(buffer, buffer.Length - count, count);

                if (rewind)
                    this.buffered.Seek(-buffer.Length, SeekOrigin.Current);

                return buffer;
            }

            void IDisposable.Dispose()
            {
            }
        }
    }
}
